<cfcomponent displayname="CrafterDocument" accessors="yes" output="no" hint="Represents a single XML document from Crafter. Provides tools both for generic element access as well as specialized media/meta methods based on the knowledge of the Full Sail / Crafter implementation.">
	
	
	<!--- Properties --->
	<cfproperty name="productCode" type="string" />
	<cfproperty name="crafterFactory" type="any" />
	<cfproperty name="crafterResource" type="any" />
	<cfproperty name="filePath" type="string" />
	<cfproperty name="documentMeta" type="any" />
	<cfproperty name="documentIsLoaded" type="boolean" />
	<cfproperty name="documentMetaIsLoaded" type="boolean" />
	<cfproperty name="analyzedForTemplatePatterns" type="boolean" default="false" />
	<cfproperty name="foundTemplatePatterns" type="boolean" default="false" />
	<cfproperty name="modifiedXML" type="string" default="" />
	
	
	<!--- init() --->
	<cffunction name="init" access="public" returntype="any" output="no">
		<cfargument name="productCode" type="string" required="yes" />
		<cfargument name="crafterFactory" type="any" required="yes" />
		<cfscript>
			
			// Set the product code for this document.  This will be used by
			// most of the other object methods to limit the range of their xPath searches
			setProductCode(arguments.productCode);

			// This is a back reference to the CrafterFactory which 
			// allows it to share methods.
			setCrafterFactory(arguments.crafterFactory);
			setCrafterResource("");

			// Because we've only initialized the object but have not tied it to a document yet
			// we mark it as not loaded.  This is also useful in case the load() operation fails,
			// the document object could still exist in a collection, but could be passed over.
			setDocumentIsLoaded(false);
			setDocumentMetaIsLoaded(false);
			setAnalyzedForTemplatePatterns(false);
			setFoundTemplatePatterns(false);
			setModifiedXML("");
			return this;
			
		</cfscript>
	</cffunction>
	
	
	<!--- load() --->
	<cffunction name="load" access="public" returntype="any" output="no">
		<cfargument name="path" type="string" required="yes" />
		<cfscript>
			
			// Set internal object filepath
			setFilePath(arguments.path);

			// CrafterResource is another Crafter rivet Java object
			if (getCrafterResource() EQ "") {
				setCrafterResource(getCrafterFactory().getMetaDataAccessor().loadResource(arguments.path));
			}
			
			// Note a successful load.
			setDocumentIsLoaded(true);
			
			// Look at the document XML for inline template patterns.
			// If found, process them.
			analyzeForTemplatePatterns(); 
			if (getFoundTemplatePatterns()){
				applyTemplates();
			}
			
		</cfscript>
	</cffunction>
	
	
	<!--- getXml() --->
	<cffunction name="getXml" access="public" returntype="string" output="no">
		<cfif (getModifiedXML() NEQ "")>
			<cfreturn getModifiedXML() />
		<cfelse>
			<cfreturn getCrafterResource().getCrafterDescriptor().getDocument().asXml() />
		</cfif>
	</cffunction>
	
	
	<!--- getMeta() --->
	<cffunction name="getMeta" access="public" returntype="crafter.CrafterDocumentMeta" output="no">
		<cfscript>
			/*
				Check to see if the meta has been loaded. If so, return it immediatately.
				If not, call processDocumentMeta() which creates a CrafterDocumentMeta object
				and a) set it and b) return it.
			*/
			if (getDocumentMetaIsLoaded()) {
				return getDocumentMeta();
			}
			else {
				setDocumentMeta(processDocumentMeta());
				setDocumentMetaIsLoaded(true);
				return getDocumentMeta();
			}
		</cfscript>
	</cffunction>
	
	
	<!--- lazyLoad() --->
	<cffunction name="lazyLoad" access="private" returntype="void" output="no" hint="">
		<cfscript>
			/*
				OK, so we want to get the value regardless of what method we pulled this document from.
				If we pulled it using a collection style, the crafter resource is going to be of type:
				
				org.rivetlogic.crafter.core.accessor.impl.NavTreeImpl
				
				in which case we can look for properties coming back from the filter.  If this document
				was loaded by itself, the crafter resource will be of type:
				
				org.rivetlogic.crafter.core.accessor.impl.ResourceRefImpl
					
				in which case we need to look at the XML and search.  This is all a part of the effort
				to allow heterogenous collections / to minimize file IO and memory needs to boost the
				performance of Crafter/CF.
			*/
			
			if (IsInstanceOf(getCrafterResource(),"org.rivetlogic.crafter.core.accessor.impl.ResourceRefImpl")) {
				// Make sure the document is loaded.
				if (NOT getDocumentIsLoaded()) {
					load(
						path = getCrafterResource().getUrl()
					);
				}		
			}
		</cfscript>
	</cffunction>
	
	
	<!--- getValue() --->
	<cffunction name="getValue" access="public" returntype="string" output="no" hint="">
		<cfargument name="xPath" type="string" required="yes" />
		<cfscript>
			lazyLoad();
			var local.returnValue = "";
			
			// Look to see if this is a NavTreeImpl in which case it could be affected by a filter. This
			// could potentially allow it to have crafter native properties all ready to go for us.  If so,
			// this is the fastest way to get the data out.
			if (IsInstanceOf(getCrafterResource(),"org.rivetlogic.crafter.core.accessor.impl.NavTreeImpl")) {
				
				// Take the xPath and grab the last piece of the path. We'll use this as our property name.
				local.xPathAsPropertyName = ListLast(arguments.xPath,"/");
				
				// Use Crafter's native getProperty() method to try and fast-pull the property out.
				local.sPropertyValue = getCrafterResource().getNavItem().getProperty(local.xPathAsPropertyName);
				
				// If there wasn't a native Crafter property, Crafter returns NULL, so to see if it was
				// succesfull we need to check for the existance of variable we set.
				if (IsDefined("local.sPropertyValue")) {
					return local.sPropertyValue;
				}
			}
			
			// There was no native Crafter property.  Now we can search the XML.
			local.arSearchResults = XmlSearch(getXml(),arguments.xPath);
			if (ArrayLen(local.arSearchResults) GT 0) {
				local.returnValue = local.arSearchResults[1].XmlText;
			}
			
			return local.returnValue;
		</cfscript>
	</cffunction>
	
	
	<cffunction name="getValues" access="public" returntype="array" output="no" hint="">
		<cfargument name="xpath" type="string" required="yes" />
		<cfscript>
			lazyLoad();
			return XmlSearch(getXml(),arguments.xPath);
		</cfscript>
	</cffunction>
	
	
	<!--- getValueAsTextObject() --->
	<cffunction name="getValueAsTextObject" access="public" returntype="crafter.CrafterText" output="no" hint="">
		<cfargument name="xPath" type="string" required="yes" />
		<cfscript>
			// Create text object, load it with the text value from getValue(), and return.
			local.oText = CreateObject("component","crafter.CrafterText").init(
				text = getValue(arguments.xPath)
			);
			return local.oText;
		</cfscript>
	</cffunction>
	
	
	<!--- getTags() --->
	<!--- Specialized method for grabbing tag data and converting to array --->
	<cffunction name="getTags" access="public" returntype="array" output="no">
		<cfargument name="type" type="string" required="no" default="system" />
		<cfscript>
			// Get the xml
			local.xml = getXml();
			
			// Do an xPath search for the tags, then based on the results
			// return either the results as an array or an empty array.
			local.arTagGroups = XmlSearch(local.xml,"//tagGroup[productCode='#getProductCode()#']/#arguments.type#Tags");
			if (ArrayLen(local.arTagGroups) GT 0) {
				return ListToArray(local.arTagGroups[1].XmlText);
			}
			else {
				local.arTags = ArrayNew(1);
				return local.arTags;
			}
		</cfscript>
	</cffunction>
	
	
	<!--- getMedia() --->
	<cffunction name="getMedia" access="public" returntype="crafter.CrafterCollection" output="no">
		<cfargument name="code" type="string" required="no" default="" />
		<cfscript>
			
			// Create empty collection
			local.oCollection = CreateObject("component","crafter.CrafterCollection").init();
			
			// Make local temp copy of parsed xml and search for mediaSetItems
			// that exist under the proper productCode
			local.xml = getXml();
			local.sXPathSearchStringStandard = "//media/mediaSet[productCode='#getProductCode()#']/mediaSetItem";
			local.sXPathSearchStringEmbed = "//media/mediaSet[productCode='#getProductCode()#']/mediaSetEmbedItem";
			if (arguments.code NEQ "") {
				local.sXPathSearchStringStandard &= "[code='#arguments.code#']";
				local.sXPathSearchStringEmbed &= "[code='#arguments.code#']";
			}
			
			local.arSearchResultsStandard = XmlSearch(local.xml,local.sXPathSearchStringStandard);
			local.arSearchResultsEmbed = XmlSearch(local.xml,local.sXPathSearchStringEmbed);
			local.arSearchResultsCombined = ArrayNew(1);
			local.arSearchResultsCombined.addAll(local.arSearchResultsStandard);
			local.arSearchResultsCombined.addAll(local.arSearchResultsEmbed);

			// Look for results
			if (ArrayLen(local.arSearchResultsCombined) EQ 0) {
				// No results. Return a collection with a single, unloaded media object.
				local.oMediaItem = CreateObject("component","crafter.CrafterMediaItem").init(
					rootPath = getCrafterFactory().getRootPath()
				);
				local.oCollection.addToCollection(item = local.oMediaItem);
				return local.oCollection;
			}
			else {
				
				// Found results. Loop over them.
				for (local.i = 1; local.i LTE ArrayLen(local.arSearchResultsCombined); local.i++) {					
					
					// Create an empty media object
					if (local.arSearchResultsCombined[i].XmlName EQ "mediaSetItem") {
						local.oMediaItem = CreateObject("component","crafter.crafterMediaItem").init(
							rootPath = getCrafterFactory().getRootPath()
						);
					}
					else if (local.arSearchResultsCombined[i].XmlName EQ "mediaSetEmbedItem") {
						local.oMediaItem = CreateObject("component","crafter.crafterMediaEmbedItem").init(
							rootPath = getCrafterFactory().getRootPath()
						);
					}
					
					
					// Load the media object
					local.oMediaItem.loadFromXml(oXML = local.arSearchResultsCombined[i]);
					
					// Add the media item to the collection
					local.oCollection.addToCollection(item = local.oMediaItem);
					
				}
				
				// Return the populated collection
				return local.oCollection;
				
			}
			
		</cfscript>
	</cffunction>
	
	
	<!--- getMediaItemsByCode() --->
	<cffunction name="getMediaItemsByCode" access="public" returntype="crafter.CrafterCollection" output="no">
		<cfargument name="code" type="string" required="yes" />
		<cfscript>
			return getMedia(arguments.code);
		</cfscript>
	</cffunction>
	
	
	<!--- hasVideo --->
	<cffunction name="hasVideo" access="public" returntype="boolean" output="no">
		<cfscript>
			// Get the parsed XML object, then search it for a media item inside a matching
			// product code group that has its isVideo flag set to yes.  If one is found,
			// return true, else false.
			local.xml = getXml();
			local.arSearchResults = XmlSearch(local.xml,"//mediaSet[productCode='#getProductCode()#']/mediaSetItem/isVideo[text()='true']");
			if (ArrayLen(local.arSearchResults) GT 0) {
				return true;	
			}
			else {
				return false;
			}
		</cfscript>
	</cffunction>
	
	
	<!--- hasRequiredTags() --->
	<cffunction name="hasRequiredTags" access="public" returntype="boolean" output="no">
		<cfargument name="tags" type="string" required="yes" />
		<cfargument name="type" type="string" required="no" default="system" />
		
		<cfscript>
			
			// This is the boolean we will return. Set to true by default, we
			// we attempt to prove it wrong.
			local.blnFoundAllTags = true;
			
			// Pull the tags out of the document.  This returns as an array, but we're
			// going to convert that array to a list.  The reason for this is that it will
			// allow us to do a string-based find operation once for each tag passed in, rather
			// than have to have multiple loops to search through the array.
			local.sTagsInDocument = ArrayToList(getTags(arguments.type));
			
			// Loop over the list of tags we passed in. Each time, do a string find (case insensitive)
			// on the tags in the document.  If nothing is found, immediately set the results to 
			// false, break out of the loop, and return.
			for (local.i = 1; local.i LTE ListLen(arguments.tags); local.i++) {
				local.sTagToSearchFor = ListGetAt(arguments.tags, local.i);
				local.blnFoundTag = FindNoCase(local.sTagToSearchFor, local.sTagsInDocument);
				if (local.blnFoundTag EQ 0) {
					local.blnFoundAllTags = false;
					break;
				}
			}
			
			return local.blnFoundAllTags;
		</cfscript>
	</cffunction>
	
	
	<!--- getInheritanceMode() --->
	<cffunction name="getInheritanceMode" access="public" returntype="any" output="no">
		<cfscript>
			
			// Attempt to grab the inheritance mode XML nodes
			local.xml = getXml();
			local.arModeData = XmlSearch(local.xml,"//inheritance-mode");
			
			// Look at the results.  If anything was return, send it back
			// otherwise, return the default mode;
			if (ArrayLen(arModeData)) {
				return arModeData[1].XmlText;
			}
			else {
				return "page-inherit";
			}
			
		</cfscript>
	</cffunction>
	
	
	<!--- processDocumentMeta() --->
	<cffunction name="processDocumentMeta" access="public" returntype="crafter.CrafterDocumentMeta" output="no">
		<cfscript>
			// Create an empty meta object, passing in a reference to this document,
			// allowing the meta object to handle the heavy lifting. Then set the object
			// as the meta for this document
			local.oMeta = CreateObject("crafter.CrafterDocumentMeta").init(
				productCode = getProductCode(),
				mode = getInheritanceMode()
			);
			local.oMeta.load(
				xml = getXml()
			);
			return local.oMeta;
		</cfscript>
	</cffunction>
	
	
	<!--- getFileFromFilePath() --->
	<cffunction name="getFileFromFilePath" access="public" returntype="string" output="no" hint="Given a file path, it returns the filename, intelligently including filenames with dots in them.">
		<cfargument name="showExt" type="boolean" required="false" default="true" />
		<cfscript>
			local.sFileFromPath = ListLast(getFilePath(),"/");
			if (arguments.showExt) {
				return local.sFileFromPath;	
			}
			else {
				local.nNumberOfDotsInFileName = ListLen(local.sFileFromPath,".");
				local.sModifiedFileName = "";
				for (local.i = 1; local.i LT local.nNumberOfDotsInFileName; local.i++) {
					local.sModifiedFileName = local.sModifiedFileName & ListGetAt(local.sFileFromPath,i,".");
				}
				return local.sModifiedFileName;
			}
		
		</cfscript>
	</cffunction> 
	
	<!--- analyzeForTemplatePatterns() --->
	<cffunction name="analyzeForTemplatePatterns" access="private" output="no" returntype="any" hint="">
		<cfset var local = {} />
		<cfscript>
			local.sPatternRegex = "\[\[[^\[\[]+?\]\]";
			local.stRegExResults = REFindNoCase(local.sPatternRegex, getXml(), 1, true);
			if (local.stRegExResults.POS[1] NEQ 0) {
				setFoundTemplatePatterns(true);
			}
			else {
				setFoundTemplatePatterns(true);
			}
			setAnalyzedForTemplatePatterns(true);
		</cfscript>
	</cffunction>
	
	
	<!--- applyTemplates() --->
	<cffunction name="applyTemplates" access="private" output="no" returntype="any" hint="">
		
		<cfset var local = {} />
		<cfscript>
			
			import org.cfcommons.templating.impl.*;
			
			// Default pattern search boolean and regex, create local copy of raw XML
			local.blnFoundAllPatterns = false;
			local.sPatternRegex = "\[\[[^\[\[]+?\]\]";
			local.sTemplatePatternRegex = "_([^>]+?)_";
			local.sXml = getXml();
											 
			// Loop until we've determined we've found all the patterns
			while (local.blnFoundAllPatterns EQ false) {
				
				// Search for the first occurance of the regex pattern
				local.stRegExResults = REFindNoCase(local.sPatternRegex, local.sXml, 1, true);
				
				// Analyze the structure to determine if anything was found
				if (local.stRegExResults.POS[1] EQ 0) {
					
					// No match was found.  Either no patterns existed or all have
					// been replaced.  Mark as such
					local.blnFoundAllPatterns = true;
				}
				
				else {
					
					// Pull out the matching text.
					local.sPatternText = Mid(local.sXml, local.stRegExResults.POS[1], local.stRegExResults.LEN[1]);
					
					// Convert the pattern text to a key struct
					// Remove brackets
					local.sItemSettings = {};
					local.sPatternText = Replace(local.sPatternText,"[[","");
					local.sPatternText = Replace(local.sPatternText,"]]","");
					
					// Loop over, as if it were a space-delimited list
					for (i=1; i LTE ListLen(local.sPatternText, " "); i++) {
						local.sPatternSetting = ListGetAt(local.sPatternText, i, " ");
						local.sItemSettings["#ListGetAt(local.sPatternSetting,1,'=')#"] = ListGetAt(local.sPatternSetting,2,'=');
					}

					try {
						// Create the full path to the template file, then pass that
						// to the cfcommons filetemplate constructor
						local.sTemplateFilePath = getCrafterFactory().getTemplatesPath() & "\" & local.sItemSettings["template"] & ".txt";
						local.oFileTemplate = new FileTemplate(
							templatePath = local.sTemplateFilePath,
							pattern = local.sTemplatePatternRegex
						);	
						
						// Run the template replace function based on whether this is a media or text item
						if (local.sItemSettings["type"] EQ "media") {
							
							// Grab the necessary media item
							local.oMediaItem = getMediaItemsByCode(local.sItemSettings["itemCode"]).getItem(1);
							local.sTemplateResults = local.oFileTemplate.replace({
								_path_ = local.oMediaItem.getAlfrescoRoot() & local.oMediaItem.getPath(),
								_height_ = local.oMediaItem.getHeight(),
								_width_ = local.oMediaItem.getWidth(),
								_alt_ = local.oMediaItem.getAlt(),
								_title_ = local.oMediaItem.getTitle(),
								_caption_ = local.oMediaItem.getCaption()
							});
						}
						else if (local.sItemSettings["type"] EQ "text") {
							local.arKeyFieldsShort = local.oFileTemplate.introspect();
							local.arKeyFieldsFull = local.oFileTemplate.introspect("full");
							
							local.stKeyStruct = {};
							for (x=1; x LTE ArrayLen(local.arKeyFieldsShort); x++) {
								StructInsert(local.stKeyStruct, local.arKeyFieldsFull[x], getValue("//#local.arKeyFieldsShort[x]#"));
							}
							local.sTemplateResults = local.oFileTemplate.replace(local.stKeyStruct);	
						}
						
						else if (local.sItemSettings["type"] EQ "embed") {
							local.sEmbedCode = getValue("//mediaSet[productCode='#getProductCode()#']/mediaSetEmbedItem[code='#local.sItemSettings["itemCode"]#']/embedHTML");
							local.sTemplateResults = local.oFileTemplate.replace({
								_embedCode_ = local.sEmbedCode
							});
						}
						
						
					}
					catch(Any e) {
						// If there was an error loading the file or loading the data (really, any error
						// applying the template) we'll catch it here and set the template results to blanks
						// so the next block of code will just replace the [[blah]] markup with nothing, cleaning
						// up our document of unsightly custom markup.
						local.sTemplateResults = "";
						writedump(e);abort;	
					}
					
					// Replace the markup
					local.sXml = REReplace(local.sXml, local.sPatternRegex, XmlFormat(local.sTemplateResults), "one");
					setModifiedXML(local.sXml);
				}
			}
		</cfscript>
	</cffunction>
	
	
	<cffunction name="getAsJSON" access="public" returntype="string" output="no">
		<cfreturn SerializeJson(getCrafterFactory().getUtil().convertXmlToStruct(xmlNode=getXml(), str=StructNew())) />
	</cffunction>
	
	

</cfcomponent>